package org.leolsean.apple;

import com.alibaba.fastjson2.JSON;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.leolsean.exception.ConfigurationException;
import org.leolsean.exception.InternalException;
import org.leolsean.exception.NotfoundException;
import org.leolsean.util.HttpClientUtil;

/**
 * <p>
 * 苹果接口调用工具类
 * </p>
 *
 * @author 李振
 * @email leo.lee1023@gmail.com
 * @since 2022-05-10
 */
@Slf4j
public class ApplePublisher {

  public static final String DEFAULT_BASE_URL = "https://api.storekit.itunes.apple.com/";
  public static final String SANDBOX_BASE_URL = "https://api.storekit-sandbox.itunes.apple.com/";
  private static final Map<String, AccessToken> PRIVATE_KEY_MAP = new ConcurrentHashMap<>(8);

  /**
   * 通过谷歌订单号 - 获取交易历史 GET {BASE_URL}/inApps/v1/history/{originalTransactionId}
   */
  public static TransactionHistory getTransactionHistory(String packageName, String keyId,
      String issuer, String originalTransactionId)
      throws ConfigurationException, NotfoundException, InternalException {
    String accessToken = accessToken(packageName, keyId, issuer);
    Map<String, String> headers = new HashMap<>();
    headers.put("Authorization", "Bearer " + accessToken);
    String apiUrl = DEFAULT_BASE_URL + "inApps/v1/history/" + originalTransactionId;
    HttpClientUtil.ResponseEntity responseEntity = HttpClientUtil
        .sendGet(apiUrl, headers, null, StandardCharsets.UTF_8.name());
    verifyErrorCode(responseEntity.getStatusCode());
    return JSON.parseObject(responseEntity.getBody(), TransactionHistory.class);
  }

  /**
   * 通过谷歌订单号 - 获取所有订阅状态 GET https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}
   */
  public static SubscriptionStatuses getAllSubscriptionStatuses(String packageName, String keyId,
      String issuer, String originalTransactionId)
      throws ConfigurationException, NotfoundException, InternalException {
    String accessToken = accessToken(packageName, keyId, issuer);
    Map<String, String> headers = new HashMap<>();
    headers.put("Authorization", "Bearer " + accessToken);
    String apiUrl = DEFAULT_BASE_URL + "inApps/v1/subscriptions/" + originalTransactionId;
    HttpClientUtil.ResponseEntity responseEntity = HttpClientUtil
        .sendGet(apiUrl, headers, null, StandardCharsets.UTF_8.name());
    verifyErrorCode(responseEntity.getStatusCode());
    return JSON.parseObject(responseEntity.getBody(), SubscriptionStatuses.class);
  }

  /**
   * 通过谷歌订单号 - 获取退款历史 GET https://api.storekit.itunes.apple.com/inApps/v1/refund/lookup/{originalTransactionId}
   */
  public static RefundOrder getRefundHistory(String packageName, String keyId, String issuer,
      String originalTransactionId)
      throws ConfigurationException, NotfoundException, InternalException {
    String accessToken = accessToken(packageName, keyId, issuer);
    Map<String, String> headers = new HashMap<>();
    headers.put("Authorization", "Bearer " + accessToken);
    String apiUrl = DEFAULT_BASE_URL + "inApps/v1/refund/lookup/" + originalTransactionId;
    HttpClientUtil.ResponseEntity responseEntity = HttpClientUtil
        .sendGet(apiUrl, headers, null, StandardCharsets.UTF_8.name());
    verifyErrorCode(responseEntity.getStatusCode());
    System.out.println(responseEntity.getBody());
    return JSON.parseObject(responseEntity.getBody(), RefundOrder.class);
  }

  /**
   * 通过用户提供的订单号 - 查询订单信息 GET https://api.storekit.itunes.apple.com/inApps/v1/lookup/{orderId}
   */
  public static Order lookUpOrderID(String packageName, String keyId, String issuer, String orderId)
      throws ConfigurationException, NotfoundException, InternalException {
    String accessToken = accessToken(packageName, keyId, issuer);
    Map<String, String> headers = new HashMap<>();
    headers.put("Authorization", "Bearer " + accessToken);
    String apiUrl = DEFAULT_BASE_URL + "inApps/v1/lookup/" + orderId;
    HttpClientUtil.ResponseEntity responseEntity = HttpClientUtil
        .sendGet(apiUrl, headers, null, StandardCharsets.UTF_8.name());
    verifyErrorCode(responseEntity.getStatusCode());
    return JSON.parseObject(responseEntity.getBody(), Order.class);
  }


  @Data
  private static class AccessToken {

    private String token;
    // 失效时间
    private long exp;

    // 是否已失效
    public boolean isExp() {
      return exp < System.currentTimeMillis();
    }
  }

  /**
   * 生成Token 先从本地缓存中获取,获取不到或者已经过期时进行实时计算
   */
  private static String accessToken(String packageName, String keyId, String issuer)
      throws ConfigurationException {
    AccessToken accessToken = Optional
        .ofNullable(
            Optional.ofNullable(PRIVATE_KEY_MAP.get(packageName)).filter(key -> !key.isExp())
                .orElseGet(() -> {
                  try {
                    AccessToken token = buildKey(packageName, keyId, issuer);
                    PRIVATE_KEY_MAP.put(packageName, token);
                    return token;
                  } catch (ConfigurationException e) {
                    return null;
                  }
                }))
        .orElseThrow(() -> new ConfigurationException(packageName + ".p8 - 文件缺失或文件格式有误!"));

    return accessToken.getToken();
  }

  /**
   * 获取请求Token
   */
  private static AccessToken buildKey(String packageName, String keyId, String issuer)
      throws ConfigurationException {
    Map<String, Object> header = new HashMap<>();
    header.put(JwsHeader.ALGORITHM, SignatureAlgorithm.ES256.getValue());
    header.put(JwsHeader.KEY_ID, keyId);
    header.put(Header.TYPE, Header.JWT_TYPE);

    Map<String, Object> claims = new HashMap<>();
    claims.put("iss", issuer);
    claims.put("iat", System.currentTimeMillis() / 1000);

    // 30分钟有效期
    long exp = System.currentTimeMillis() + 30 * 60 * 1000;
    claims.put("exp", exp / 1000);
    claims.put("aud", "appstoreconnect-v1");
    claims.put("bid", packageName);

    // System.out.println("header: "+ JSON.toJSONString(header));
    // System.out.println("payload: "+ claims);
    String token = Jwts.builder()
        .setHeader(header)
        .setClaims(claims)
        .signWith(SignatureAlgorithm.ES256, getECPrivateKey(packageName))
        .compact();

    AccessToken key = new AccessToken();
    key.setToken(token);
    key.setExp(exp);
    return key;
  }


  /**
   * 获取PrivateKey对象
   */
  private static PrivateKey getECPrivateKey(String packageName) throws ConfigurationException {
    InputStream inputStream = null;
    try {
      Security.addProvider(new BouncyCastleProvider());
      KeyFactory keyFactory = KeyFactory.getInstance("ECDH", "BC");
      inputStream = Thread.currentThread().getContextClassLoader()
          .getResourceAsStream("apple_api_cert/" + packageName + ".p8");
      assert inputStream != null;
      byte[] devicePriKeybytes = toByteArray(inputStream);
      PKCS8EncodedKeySpec devicePriKeySpec = new PKCS8EncodedKeySpec(devicePriKeybytes);
      return keyFactory.generatePrivate(devicePriKeySpec);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (inputStream != null) {
        try {
          inputStream.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    throw new ConfigurationException("苹果接口秘钥缺失或内容有误!");
  }

  public static byte[] toByteArray(InputStream input) throws IOException {
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int n;
    while (-1 != (n = input.read(buffer))) {
      output.write(buffer, 0, n);
    }
    return output.toByteArray();
  }

  public static class Model {

    public <T> List<T> decodePayload(String[] tokens, int index, Class<T> clz) {
      if (tokens == null || tokens.length < index + 1) {
        return null;
      }
      List<T> list = new ArrayList<>();
      Base64.Decoder decoder = Base64.getUrlDecoder();
      if (index == -1) {
        for (String token : tokens) {
          String[] chunks = token.split("\\.");
          list.add(JSON.parseObject(new String(decoder.decode(chunks[1])), clz));
        }
        return list;
      } else {
        String[] chunks = tokens[index].split("\\.");
        //String header = new String(decoder.decode(chunks[0]));
        list.add(JSON.parseObject(new String(decoder.decode(chunks[1])), clz));
        return list;
      }
    }
  }

  @EqualsAndHashCode(callSuper = true)
  @Data
  public static class TransactionHistory extends Model {

    /**
     * 您在查询中使用的令牌，用于从Get Transaction History端点请求下一组事务。
     */
    private String revision;
    /**
     * 应用程序的捆绑标识符。
     */
    private String bundleId;
    /**
     * 应用程序在 Main Store 中的标识符。
     */
    private String appAppleId;
    /**
     * 您发出请求的服务器环境，无论是沙盒还是生产环境。
     */
    private String environment;
    /**
     * 一个布尔值，指示 Main Store 的事务是否多于此请求中返回的事务。
     */
    private boolean hasMore;
    /**
     * 客户的一系列应用内购买交易，由 Apple 签名，采用 JSON Web 签名格式。
     */
    private String[] signedTransactions;

  }


  /**
   * 返回码验证
   */
  private static void verifyErrorCode(int statusCode)
      throws ConfigurationException, NotfoundException, InternalException {
    switch (statusCode) {
      case HttpStatus.SC_OK:
        log.debug("苹果接口调用成功!");
        return;
      case HttpStatus.SC_ACCEPTED:
        log.debug("苹果接口调用成功!");
        return;
      case HttpStatus.SC_BAD_REQUEST:
        throw new ConfigurationException("可能因配置问题无法访问Apple接口.");
      case HttpStatus.SC_UNAUTHORIZED:
        throw new ConfigurationException("可能因配置问题无法访问Apple接口.");
      case HttpStatus.SC_NOT_FOUND:
        throw new NotfoundException("接口或信息不存在.");
      case HttpStatus.SC_INTERNAL_SERVER_ERROR:
        throw new InternalException("网络异常");
      default:
        throw new InternalException("网络异常");
    }
  }


  /**
   * 订阅组中所有订阅的最新签名交易信息和签名续订信息的数组。
   */
  @Data
  public static class SubscriptionStatuses {

    /**
     * 应用程序的捆绑标识符。
     */
    private String bundleId;
    /**
     * 应用程序在 Main Store 中的标识符。
     */
    private String appAppleId;
    /**
     * 您发出请求的服务器环境，无论是沙盒还是生产环境。
     */
    private String environment;

    /**
     * 订阅组中所有订阅的最新签名交易信息和签名续订信息的数组。
     */
    private List<SubscriptionData> data;

    /**
     * data节点
     */
    @Data
    public static class SubscriptionData {

      /**
       * 数组中订阅的订阅组标识符
       */
      private String subscriptionGroupIdentifier;

      /**
       * 订阅组中所有订阅的最新签名交易信息和签名续订信息的数组。
       */
      private List<LastTransactions> lastTransactions;

      /**
       * 订阅组中所有订阅的最新签名交易信息和签名续订信息的数组。
       */
      @EqualsAndHashCode(callSuper = true)
      @Data
      public static class LastTransactions extends Model {

        /**
         * 订阅的原始交易标识符
         */
        private String originalTransactionId;
        /**
         * 订阅状态
         */
        private String status;
        /**
         * Apple 签署的订阅续订信息，采用 JSON Web Signature (JWS) 格式。
         */
        private String signedRenewalInfo;

        /**
         * Apple 签名的交易信息，JWS 格式。
         */
        private String signedTransactionInfo;

      }
    }
  }

  /**
   * 订阅组中所有订阅的最新签名交易信息和签名续订信息的数组。
   */
  @EqualsAndHashCode(callSuper = true)
  @Data
  public static class Order extends Model {

    /**
     * 指示订单 ID 是否有效的状态。。
     */
    private String status;
    /**
     * 一系列应用内购买交易，这些交易是订单的一部分，由 Apple 签名，采用 JSON Web 签名格式。。
     */
    private String[] signedTransactions;
  }

  /**
   * 订阅组中所有订阅的最新签名交易信息和签名续订信息的数组。
   */
  @EqualsAndHashCode(callSuper = true)
  @Data
  public static class RefundOrder extends Model {

    /**
     * 一系列应用内购买交易，这些交易是订单的一部分，由 Apple 签名，采用 JSON Web 签名格式。。
     */
    private String[] signedTransactions;
  }
  /**
   * 延长订阅续订日期
   */
  // public Object extendASubscriptionRenewalDate() {
  // }
}
